<template>
    <div class="file-upload-container" h-full w-full>
        <div class="drag-file-part" ref="dragRef" @drop="handleDrop" @dragover="handleDragOver">
            <span>拖拽文件或者文件夹到此进行上传</span>
        </div>
        <div class="upload-btn" mt12>
            <input type="file" ref="fileRef" class="file-input" multiple @change="selectFiles" />
            <input type="file" ref="filesRef" class="file-input" multiple @change="selectFiles" webkitdirectory />
            <el-button mr10 type="primary" @click="handleClickFile">上传文件</el-button>
            <el-button type="primary" @click="handleClickFiles">上传文件夹</el-button>
        </div>
        <el-table :data="fileList" mt12 height="350px" border>
            <el-table-column label="文件名" prop="name"></el-table-column>
            <el-table-column label="文件大小" prop="size">
                <template #default="{ row }">
                    {{ (row.size / 1024 / 1024) >= 1 ? (row.size / 1024 / 1024).toFixed(2) + 'MB' : (row.size /
                        1024).toFixed(2) + 'KB' }}
                </template>
            </el-table-column>
            <el-table-column label="文件类型" prop="type"></el-table-column>
            <el-table-column label="上传进度" prop="progress">
                <template #default="{row}">
                    <el-progress :text-inside="true" :stroke-width="15" :percentage="row.progress" ></el-progress>
                </template>
            </el-table-column>
            <el-table-column label="操作">
                <template #default="{ row }">
                    <el-button mr10 type="text" @click="handleSuspend(row)" :disabled="row.progress == 100">暂停</el-button>
                    <el-button type="text" @click="handleContinue(row)" :disabled="row.progress == 100">继续</el-button>
                </template>
            </el-table-column>
        </el-table>
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import SparkMd5 from 'spark-md5'

const fileList = ref<any[]>([])
const selectedFiles = ref<any[]>([])

const fileRef = ref<HTMLInputElement>()
const filesRef = ref<HTMLInputElement>()
const dragRef = ref<HTMLDivElement>()

const handleClickFile = () => {
    fileRef.value?.click()
}

const handleClickFiles = () => {
    filesRef.value?.click()
}

interface FileType {
    file:File,
    name: string,
    size: string,
    type: string,
    chunks:Array<Blob>,
    filehash:string,
    startFlag:boolean,
    progress:number
}

const selectFiles = async (e: any) => {
    if (!e.target?.files.length) {
        return
    }
    selectedFiles.value = selectedFiles.value.concat([...e.target?.files])
    fileList.value = selectedFiles.value.map(file => ({
        file,
        name:file.name,
        size:file.size,
        type:file.type,
        chunks: splitChunks(file),
        filehash: '',
        startFlag:true,
        progress:0
    }))
    // const chunks = splitChunks(fileList.value[0])
    // filehash.value = await setFileHash(fileList.value[0])
    fileList.value.forEach((file:FileType) => {
        handleUpload(file)
    })
}
const handleUpload = async (file:FileType) => {
    file.filehash = await setFileHash(file.file)
    const valid = await validateFile(file)
    if (valid.data.isExist) {
        //NotifyError('该文件已经上传，请勿重复上传。')
        file.progress = 100
        return
    }
    //valid.data.existsFiles为已经上传的分片
    uploadChunks(file, valid.data.existsFiles)
}

//大文件上传：1.文件分片
const CHUNK_SIZE = 1024*1024 //1MB
const splitChunks = (file:File) => {
    let start = 0
    let chunks:Blob[] = []
    while(start < file.size){
        chunks.push(file.slice(start,start + CHUNK_SIZE))
        start += CHUNK_SIZE
    }
    return chunks
}
//2.计算文件hash值
const setFileHash = (file:File) => {
    return new Promise<string>(resolve => {
        const fileReader = new FileReader()
        fileReader.readAsArrayBuffer(file)
        fileReader.onload = (e) => {
            const hash = new SparkMd5.ArrayBuffer()
            hash.append(e.target?.result as ArrayBuffer)
            resolve(hash.end())
        }
    })
}
//3.文件上传
const uploadChunks = async (fileItem: FileType,existsFiles:string[]) => {
    const requests = fileItem.chunks.map((chunk,index) => ({
        fileHash: fileItem.filehash,
        chunkHash: fileItem.filehash +'-'+index,
        chunk,
    })).filter(item => !existsFiles.includes(item.chunkHash)) //过滤掉已经上传的分片
    .map(file => {
        const formData = new FormData()
        formData.append('file', file.chunk)
        formData.append('fileHash', file.fileHash)
        formData.append('chunkHash', file.chunkHash)
        return formData
    }).map(formData => () => {
        return fetch('http://localhost:3000/upload', {
            method: 'POST',
            body: formData,
            mode: 'no-cors'
        })
    })
    concurrentRequest(fileItem,requests,6).then(res => {
        //文件合并
        mergeFile(fileItem)
    })

}
//4.控制并发上传个数,google同时最多6个请求
const concurrentRequest = (fileItem:FileType,requests: Array<() => Promise<Response>>,maxCount:number) => {
    return new Promise((resolve,rejects) => {
        if (requests.length == 0) {
            resolve([])
            return
        }

        let response:Response[] = []
        //已经执行的函数数量
        let index = 0
        //已经执行完的函数数量
        let count = 0
        //单个分片上传进度
        const progress = Math.floor(100 / fileItem.chunks.length)
        const upload = () => {
            if(index == requests.length || !fileItem.startFlag){
                return
            }
            const fn = requests[index]
            index++
            fn().then(res => {
                response[index] = res
            }).catch(err => {
                response[index] = err
            }).finally(() => {
                count++
                fileItem.progress += progress
                if(count == requests.length){
                    fileItem.progress = 100
                    resolve(response)
                    return
                }
                //暂停上传
                if(!fileItem.startFlag){
                    rejects()
                    return
                }
                upload()
            })
        }

        let concurrents = Math.min(maxCount,requests.length)
        for (let i = 0; i < concurrents; i++) {
            upload()
        }
    })
}
//5.文件合并
const mergeFile = (file:FileType) => {
    fetch('http://localhost:3000/merge',{
        method: 'POST',
        //设置请求头后不允许设置mode为跨域
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            fileHash:file.filehash,
            fileName:file.name
        }),
    })
}
//6.校验文件是否存在服务器，存在就不上传
const validateFile = (file:FileType) => {
    return new Promise<{code:string,message:string,data:any}>((resolve, reject) => {
        fetch('http://localhost:3000/validate', {
            method: 'POST',
            //设置请求头后不允许设置mode为跨域
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                fileHash: file.filehash,
                fileName: file.name,
            }),
        }).then(res => {
            res.json().then(res => {
                resolve(res)
            })
        })
    })
}
//暂停上传
const handleSuspend = (row:FileType) => {
    row.startFlag = false
}

//继续上传
const handleContinue = (row:FileType) => {
    row.startFlag = true
    handleUpload(row)
}

//拖拽文件
const handleDrop = (e: any) => {
    e.preventDefault()
    const arr: any[] = []
    arr.forEach.call(e.dataTransfer.items, (item) => {
        getDropFlies(item)
    })
}

//获取文件
const getDropFlies = (item: DataTransferItem) => {
    const entry = item.webkitGetAsEntry() as FileSystemEntry
    if(entry.isFile){
        selectedFiles.value.push(item.getAsFile())
    }else{
        getFileFromDirection(entry as FileSystemDirectoryEntry)
    }
}

//递归文件夹
const getFileFromDirection = (entry: FileSystemDirectoryEntry) => {
    (entry as FileSystemDirectoryEntry).createReader().readEntries((entries: any) => {
        entries.forEach((item: any) => {
            if (item.isFile) {
                item.file((file: File) => {
                    selectedFiles.value.push(file)
                })
            } else {
                getFileFromDirection(item)
            }
        })
    })
}

const handleDragOver = (e: any) => {
    e.preventDefault()
}
</script>

<style scoped lang="scss">
.file-upload-container {
    padding: 12px;

    .drag-file-part {
        height: 40%;
        border: 5px dotted var(--el-border-color);
        border-radius: 10px;
        font-size: 20px;
        color: var(--el-text-color-primary);
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .upload-btn {
        .file-input {
            display: none;
        }
    }
}
</style>